/**
 * Main orchestrating logic for the application.
 * @author birdeau@yahoo.com
 */
jsx3.lang.Package.definePackage("gicx", function(gicx) {

  //private (uniquely identifies queries that are part of a search session) -- since I can search multiple regions at a time; helps to restore all associated queries
  var searchSessionId;

  //converts results list to cdf
  var intDumbnailSize = 100;
  var queryInc = 0;
  gicx.auto_scroll_interval;
  gicx.thumb_timeout;
  gicx.active_query;
  gicx.default_postto_url = "https://post.craigslist.org/";
  gicx.MULTI_REGION_SEARCH;



  /**
   * 'main' event for the application. called by the onload event for the application
   */
  gicx.init = function() {
    //load CSS file used by CL to style content
    gicx.APP.loadInclude("http://www.craigslist.org/styles/craigslist.css","clcss","css",false);

    //only load content, ui, etc if not in test (gipp) mode.  When it test mode, the test suite will define what to do and when
    if(gicx.APP.getEnv("gipp") != "true") {
      //load those JavaScript files that will eventually be used during the life-cycle of the application
      window.setTimeout(function() { preFetchLogic(); }, 2500);

      //load the main UI
      gicx.APP.getBodyBlock().load("components/_main.xml",false);

      //hide controls that user shouldn't see until they complete various on-screen fields
      gicx.APP.getJSXByName("blkSplitMain").setVisibility("hidden");
      gicx.APP.getJSXByName("blkCategories").setVisibility("hidden");
      gicx.APP.getBodyBlock().repaint();

      //perform subscriptions to key events
      gicx.APP.subscribe("POSTING_WAS_FAVORITED",gicx.onFavorite);
      gicx.APP.subscribe("POSTING_WAS_UNFAVORITED",gicx.onUnfavorite);
      gicx.APP.subscribe("POSTING_WAS_BLOCKED",gicx.onBlock);
      gicx.APP.subscribe("POSTING_WAS_UNBLOCKED",gicx.onUnblock);
      gicx.APP.subscribe("SEARCH_WAS_ARCHIVED",gicx.onSearchArchive);
      gicx.APP.subscribe("SEARCH_WAS_UNARCHIVED",gicx.onSearchUnarchive);

      //register the hotkey that will load all images (ctrl+shift+a)
      gicx.APP.registerHotKey(gicx.prefetchAllImages,65,true,true,false);

      //register hotkey to select all records (ctrl+a)
      gicx.APP.registerHotKey(function() {
        var objMatrix = gicx.getResultsTable();
        var ids = objMatrix.getSortedIds();
        for(var i=0;i<ids.length;i++)
          objMatrix.insertRecordProperty(ids[i],"jsxselected","1",false);
        objMatrix.repaintData();
      },65,false,true,false);

      //restore session data (for now stored via dojo.storage/flash)
      gicx.restoreSessionData();

      //restore the user's preferences
      gicx.getRegionPrefs();
      gicx.showRegionPrefs();
      gicx.restoreMRUPrefs();
      gicx.getUserPrefs();
      gicx.getCategoryPrefs();
      gicx.showCategoryPrefs();

      //TO DO: need to check dates of blocked posts and purge after 2 weeks

      //advance the application if user was here before
      if(gicx.APP.getJSXByName("blkRegions").getChildren().length > 1)
        gicx.showStage2();
      if(gicx.APP.getJSXByName("blkCategories").getChildren().length > 1)
        gicx.showStage3();

      //asynchronously load the google map module
      //if(window.location.href.toLowerCase().indexOf("file") == -1)
      //  gicx.APP.loadInclude("http://maps.google.com/maps?file=api&amp;v=2&amp;key=ABQIAAAAJxqSdTZ58Lguq7exYG0v-RRJkjVtG5kvv9cZc9lArQ1JOsc9iRS_E3GYfK_DdQVvg6xYcoxTMjQYlQ","googoo","script",false);
    }
  };


  /**
   * Asynchronously preloads various logic files that will eventually be requested during app use. If the resource is needed before
   * it is fully loaded, the synchronous loader (jsx3.require) will ensure that it is loaded.  Provides a better user experience, in that it
   * allows the app to load more quickly with the minimum of logic, while pre-fetching other files, so the user gets immediate feedback
   * later in the session when they actually need a given module
   */
  function preFetchLogic() {
    var filesToLoad = ["Posting.js","Contact.js","Audit.js"];
    for(var i=0;i<filesToLoad.length;i++)
      gicx.APP.loadInclude(gicx.APP.resolveURI("js/gicx/" + filesToLoad[i]),filesToLoad[i],"script",false);
  };


  /**
   * Restores the hard cache.  The hard cache persists the user's information between browser sessions by using dojo.storage.
   * Also clears the soft cache from the previous session. Refer to the class, gicx.Cache for information on how the data
   * is being persisted.  Also note that dojo storage does not need to be used and the gicx.Cache could be modified
   * to use native browser session data (fx2+ or ie6+)
   */
  gicx.restoreSessionData = function() {
    if(gicx.APP.getEnv("vanilla") == "true") {
      //always clear the cache--even when in safe mode
      if(gicx.Cache.isEnabled())
        dojo.storage.put("_searchHistory","");
      var objSearchHistory = jsx3.xml.CDF.Document.newDocument();
      objSearchHistory.load(gicx.APP.resolveURI("xml/searchHistory.xml"));
    } else {
      var objSearchHistory;
      if(gicx.Cache.isEnabled()) {
        //restore the 'searchHistory' document. This is the user's session info
        jsx3.log("dojo storage is enabled.");

        //get the cached history from dojo storage
        var objSearchHistory = gicx.Cache.getDocument("searchHistory");

        //just in case the browser crashed, clear up session cache (never ask for the same posting twice in a session); this cache is every posting every viewed
        dojo.storage.put("_searchHistory","");
      }

      if(!objSearchHistory || !objSearchHistory.selectSingleNode("/data/favorites")) {
        //the search history doesn't exist; create here instead of restoring from dojo
        var objSearchHistory = jsx3.xml.CDF.Document.newDocument();
        objSearchHistory.load(gicx.APP.resolveURI("xml/searchHistory.xml"));
      }
    }

    //place the document in the cache (any time it is updated, it will be persisted back to the hard cache using dojo storage.
    gicx.APP.getCache().setDocument("searchHistory",objSearchHistory);
  };



  /**
   * Removes all saved items from one of the three cache categories in the 'hard cache'.
   * For example, if the user wants to remove their search history, this is the method that gets called
   * @param strType {String} one of: favorites, blocked, searches
   */
  gicx.resetSessionData = function(strType) {
    //remove the child reccords from the given category in the hard cache
    var objType = gicx.Cache.getDocument("searchHistory").selectSingleNode("/data/" + strType);
    if(objType)
      objType.removeChildren();

    //save the updated document back to the hard cache to persist
    gicx.Cache.serialize("searchHistory");

    //publish the event (so affected gui clients will know to paint)
    switch (strType){
      case "favorites":
        gicx.publish({subject:"POSTING_WAS_UNFAVORITED"});
        break;
      case "blocked":
        gicx.publish({subject:"POSTING_WAS_UNBLOCKED"});
        break;
      case "searches":
        gicx.publish({subject:"SEARCH_WAS_UNARCHIVED"});
        break;
    }
  };



  /**
   * Ensures proper reporting and logging tools can be implemented.  Allows the message to be inspected before forwarding
   * Helps with long term maintenance of the app
   * @param objEvent {Object} event object
   */
  gicx.publish = function(objEvent) {
    jsx3.log("Dispatching event: " + objEvent.subject);
    gicx.APP.publish(objEvent);
  };



  /**
   * If both a single category and a single region are being searched and the search result count is larger than 100,
   * a link is provided in the UI that the user can click to load the next 100 results.
   */
  gicx.searchNext = function() {
    var objRequest = gicx.active_query;
    if(objRequest) {
      if(objRequest.openForNext())
        objRequest.send();
    } else {
      gicx.search();
    }
  };



  /**
   * If both a single category and a single region are being searched and the search index is greater than 1/100, gets the prev result set
   */
  gicx.searchPrevious = function() {
    var objRequest = gicx.active_query;
    if(objRequest) {
      if(objRequest.openForPrevious())
        objRequest.send();
    } else {
      gicx.search();
    }
  };


  /**
   * Called when a new search is about to begin.  A search session can contain multiple queries, since more than one category
   * and more than one region/subregion/neighborhood can also be searched at a time.
   */
  gicx.resetSearchSession = function() {
    searchSessionId = (new Date()).valueOf();
    gicx.APP.getCache().clearById("search_results");
  };


  /**
   * Every search session is stored in the user's favorites.  If the user dbl-clicks on one of their saved queries,
   * this method is called to re-execute that search. This is the app-paradigm equivalent of a bookmark in the
   * doc-centric Web
   * @param objRecord {jsx3.xml.Entity} CDF record (the saved search)
   */
  gicx.restoreSearchSession = function(objRecord) {
    if(objRecord) {
      gicx.resetSearchSession();

      var objRec = objRecord.cloneNode(true);
      objRec.removeAttribute("jsxgroupname");
      objRec.removeAttribute("jsxselected");
      objRec.removeAttribute("datetime");
      var objRequest = gicx.Query.newFromRecord(objRec);
      if(objRequest) {
        //send the restored query
        objRequest.openForSearch(true);
        objRequest.subscribe(jsx3.net.Request.EVENT_ON_RESPONSE,gicx,"searchCB");
        objRequest.send();
      }
    }
  };


  /**
   * Initiates the search. Leverages the gicx.Query class to perform the actual communications with craigslist. Refer to that
   * class for a detailed view of its APIs.  If craigslist does change their search APIs, modify that class and then
   * return to this method to implement those changes.
   */
  gicx.search = function() {
    //reset the session
    gicx.resetSearchSession();

    //query the user settings for which regions to search; if a region is selected, ignore the sub-regions that belong to it (just search the region)
    var objXMLR = gicx.APP.getCache().getDocument("regions");
    objXMLR.setSelectionLanguage("XPath");
    var objRegionsToSearch = objXMLR.selectNodes("//record[@selected='1' and not(preceding-sibling::record[@all='1' and @selected='1'] or ../preceding-sibling::record[@all='1' and @selected='1'])]");

    //query the user settings for which categories to search; if a category is selected, ignore the sub-cats that belong to it
    var objXMLC = gicx.APP.getCache().getDocument("categories");
    objXMLC.setSelectionLanguage("XPath");
    var objCatsToSearch = objXMLC.selectNodes("//record[@selected='1' and not(preceding-sibling::record[@all='1' and @selected='1'] or ../preceding-sibling::record[@all='1' and @selected='1'])]");

    //loop to query; each iteration of the loop results in a separate query to cl. Each query result is appended to the final list
    gicx.MULTI_REGION_SEARCH = objRegionsToSearch.getLength() * objCatsToSearch.getLength() > 1;
    if(objRegionsToSearch.getLength() * objCatsToSearch.getLength()) {
      for(var cats=0;cats<objCatsToSearch.getLength();cats++) {
        var strCategory = objCatsToSearch.getItem(cats).getAttribute("jsxid");
        for(var regions=0;regions<objRegionsToSearch.getLength();regions++) {
          var objNode = objRegionsToSearch.getItem(regions);
          var strId = objNode.getAttribute("jsxid");
          if(objNode.getAttribute("type") == "r") {
            //this is a regional search with no subregions
            gicx._search(strId,null,null,strCategory);
          } else if(objNode.getAttribute("type") == "sr") {
            //this is a subregional search; pass both the subregion and region
            var strRegion = (objNode.getParent().getParent().getAttribute("jsxgroupname")) ?
                            objNode.getParent().getParent().getFirstChild().getAttribute("jsxid") :
                            objNode.getParent().getFirstChild().getAttribute("jsxid");
            gicx._search(strRegion,strId,null,strCategory);
          } else if(objNode.getAttribute("type") == "n") {
            //this is a neighborhood search; pass the subregion and region and neighborhood
            gicx._search(objNode.getParent().getParent().getFirstChild().getAttribute("jsxid"),objNode.getParent().getFirstChild().getAttribute("jsxid"),strId,strCategory);
          }
        }
      }
    }
  };


  /**
   * Called by 'search'.  Represents a single query that is part of a search session.  Since a search session can contain
   * multiple queries, this method may be called multiple times.
   */
  gicx._search = function(strRegion,strSubRegion,strNeighborhood,strCategory) {
    //instance the query object
    var objRequest = new gicx.Query("cx" + queryInc++);

    //set region/subregion and category
    objRequest.setRegion(strRegion);
    objRequest.setSubregion(strSubRegion);
    if(strNeighborhood)
      objRequest.setParam("neighborhood",strNeighborhood);
    objRequest.setCategory(strCategory);

    //set the parameters that comprise the query string
    var objSearchFields = gicx.APP.getJSXByName("blkAllSearchFields").getDescendantsOfType(jsx3.gui.Form);
    for(var i=0;i<objSearchFields.length;i++) {
      var objField = objSearchFields[i];
      if(objField instanceof jsx3.gui.TextBox) {
        var PARAM = objField.getValue();
        //do not include 'default' values that prompt user to enter info such as "enter keywords...", "max age", "min age", etc.
        if(!jsx3.util.strEmpty(objField.defaultlabeltext) && PARAM == objField.defaultlabeltext)
          PARAM = "";
        if(!jsx3.util.strEmpty(PARAM))
          objRequest.setParam(objField.getName(),PARAM);
      } else if(objField instanceof jsx3.gui.ImageButton) {
        //if a checkbox (cl sometimes makes a checkbox use a value (not just 01 as if both name and value provide uniqueness and state. dont' know why...)
        if(objField.getState()) {
          var strFieldName = objField.truename?objField.truename:objField.getName();
          if(objRequest.getParam(strFieldName)) {
            //don't send duplicate-named fields (by-owner and by-dealer cancel each other out)
            objRequest.removeParam(strFieldName);
          } else {
            objRequest.setParam(strFieldName,objField.truevalue?objField.truevalue:"1");
          }
        }
      }
    }

    //send directly to cl or use yahoo pipes as x-domain transport (a backup in case cl doesn't like proxied traffic coming from my own site)
    gicx.APP.getJSXByName("blkSearchResultsPad").showMask("<div style='font-size:12px;font-weight:bold;'>Searching Craigslist...</div>");
    if(gicx.APP.getEnv("pipes") == "pipes") {
      objRequest.subscribe(jsx3.net.Request.EVENT_ON_RESPONSE,gicx,"searchCB");
      objRequest.sendWithPipes()
    } else {
      objRequest.openForSearch(true);
      objRequest.subscribe(jsx3.net.Request.EVENT_ON_RESPONSE,gicx,"searchCB");
      objRequest.send();
    }
  };



  /**
   * Callback when making a search request. Converts the response (HTML page returned from craigslist) into
   * a more-useable object (an XML document with structured records)
   * @param objEvent {Object} event object, with named property, 'target', a pointer to the gicx.Query instance that did the request transport
   * @see gicx.Filter
   */
  gicx.searchCB = function(objEvent) {
    var objRequest = objEvent.target;

    //cache the search profile
    var objRecord = objRequest.serialize(true);

    //add information about the search group (a group of related queries all issued simultaneously)
    objRecord.setAttribute("jsxgroupname",searchSessionId);
    var objRecordParent = gicx.Cache.getDocument("searchHistory").selectSingleNode("//searches");
    objReturn = gicx.Cache.setRecord("searchHistory", objRecord, objRecordParent);
    gicx.publish({subject:"SEARCH_WAS_ARCHIVED",target:objReturn});

    //get the Craigslist 'list' format as a CDF Document
    var objFilter = new gicx.Filter(objRequest.getResponseText());

    //append to existing doc when a multi-region search
    var objDoc = objFilter.getPostings((gicx.MULTI_REGION_SEARCH) ? gicx.APP.getCache().getDocument("search_results") : null);

    //show user the number of search results appropriate to what the user searched for and whether any results were returned
    gicx.APP.getJSXByName("blkSearchResultsPad").hideMask();
    var objRange = objFilter.getRange();
    if(objRequest.pipes) {
      //yahoo was the transport; only way to get the result count is to count the records
      var intTotal = objDoc.selectNodes("//record").getLength();
      var strText = "Viewing " + intTotal + " RSS results";
      if(intTotal == 0) {
        gicx.APP.getJSXByName("blkSearchResultsPad").showMask("<div style='font-size:12px;font-weight:bold;'>No postings found. Try modifying search criteria.</div>");
        objDoc = jsx3.xml.CDF.Document.newDocument();
      }
      delete gicx.active_query;
    } else if(!gicx.MULTI_REGION_SEARCH && !isNaN(objRequest.indexquery)) {
      //show the 'prev' link if on page 1 or greater
      var strPrev = (objRequest.indexquery > 0) ?
        '<a href="javascript:gicx.searchPrevious();" class="paginator_link">&lt; Prev</a> | ' : '&lt; Prev | ';
      var strNext = ' | <a href="javascript:gicx.searchNext();" class="paginator_link"">Next &gt;</a>';
      gicx.active_query = objRequest;
      var strText = strPrev + "Viewing " + (objRequest.indexquery + 1) + " to " + (objRequest.indexquery + 100) + strNext;
    } else if(!gicx.MULTI_REGION_SEARCH) {
      var strPrev = (objRequest.getParam(gicx.Query.START) > 0) ?
        '<a href="javascript:gicx.searchPrevious();" class="paginator_link">&lt; Prev</a> | ' : '&lt; Prev | ';
      var strNext = (objRange.end - 0 < objRange.total - 0) ?
        ' | <a href="javascript:gicx.searchNext();" class="paginator_link">Next &gt;</a>' : ' | Next &gt;' ;
      var intEnd = objRange.end;
      var intTotal = objRange.total;
      gicx.active_query = objRequest;
      if(intTotal == 0) {
        gicx.APP.getJSXByName("blkSearchResultsPad").showMask("<div style='font-size:12px;font-weight:bold;'>No postings found. Try modifying search criteria.</div>");
        objDoc = jsx3.xml.CDF.Document.newDocument();
      }
      var strText = strPrev + "Viewing " + objRange.start + " to " + intEnd + " of " + intTotal + " " + strNext;
    } else {
      delete gicx.active_query;
      var strText = "Viewing " + objDoc.selectNodes("//record").getLength() + " Merged Records";
    }
    gicx.APP.getJSXByName("blkSearchProfile").setText(strText,true);

    //cache and repaint
    gicx.APP.getCache().setDocument("search_results",objDoc);
    var objMatrix = gicx.getResultsTable();
    objMatrix.synchronizeVScroller();
    objMatrix.repaint();

    //this will be empty, because it only lists images which haven't been scraped yet
    var objBX = gicx.APP.getJSXByName("blkxSearchResults");
    if(objBX.getDisplay() != "none")
      objBX.repaint();
  };


  /**
   * Called when user enters a hotkey (ctrl+shift+a) to auto-load all pictures for all listed postings
   */
  gicx.prefetchAllImages = function() {
    var objMatrix = gicx.getResultsTable();
    var ids = objMatrix.getSortedIds();

    //I've disabled this setting for now, but if the user prefs are set, so that this evaluates to 'true', then all images will be resolved immediately
    for(var i=0;i<ids.length;i++)
      gicx._prefetchContent(objMatrix,ids[i],(i % 10 == 0)?i:parseInt(i/10));
  };



  /**
   * Called when the user clicks on the 'view images...' link. Loads the images
   * @param strId {String} jsxid for record being hovered over (also the url for the page)
   * @param strJsxId {String} jsxid for Matrix instance containing the record
   */
  gicx.prefetchImages = function(strId,strJsxId) {
    var objMatrix = gicx.APP.getJSX(strJsxId);
    var objRecord = objMatrix.getRecordNode(strId);
    if(objRecord)
      gicx.prefetchContent(objMatrix,strId);
  };



  /**
   * Helps to throttle how the browser sends out multiple requests (stops the CPU from thrashing too much by inserting delays). This method
   * would be called by a method that needed to call prefetchContent repeatedly.  Call this method instead to create some delays.
   * @param objMatrix {jsx3.gui.Matrix}
   * @param strId {String} idf of CDF record (posting) for which to prefetch content
   * @param intIndex {int} incremented delay
   */
  gicx._prefetchContent = function(objMatrix,strId,intIndex) {
    window.setTimeout(function() { gicx.prefetchContent(objMatrix,strId); },(intIndex+1)*200);
  };


  /**
   * Requests a given posting; when the posting returns, the posting body, header, and images will be scraped
   * @param objMatrix {jsx3.gui.Matrix}
   * @param strId {String}
   */
  gicx.prefetchContent = function(objMatrix,strId) {
    var objRecord = objMatrix.getRecordNode(strId);
    if(objRecord && jsx3.util.strEmpty(objRecord.getAttribute("hasCachedImages"))) {
      //the posting hasn't been screenscraped yet for the images; do here
      var objHttp = new gicx.Query("cx" + queryInc++);
      objHttp.setURL(strId);
      objHttp.openForPage(true);
      objHttp.subscribe(jsx3.net.Request.EVENT_ON_RESPONSE,gicx.prefetchContentCB);
      objHttp.expando_parent_id = strId;
      objHttp.expando_matrix_id = objMatrix.getId();
      objHttp.send();
    } else if(objRecord && objRecord.getAttribute("hideCachedImages") == 1 && objRecord.getAttribute("hasCachedImages") == 1) {
      //the posting has already been scraped, but there is a blocking flag; remove it and repaint the cell
      objMatrix.deleteRecordProperty(strId,"hideCachedImages",false);
      objMatrix.redrawMappedCells(strId, "pic");
      window.clearTimeout(gicx.thumb_timeout);
      gicx.thumb_timeout = window.setTimeout(function()  { if(gicx.APP.getJSXByName("blkxSearchResults").getDisplay() != "none") gicx.APP.getJSXByName("blkxSearchResults").repaint(); },500);
    }
  };


  /**
   * Callback method that is notified when craigslist returns a specific posting.  Will try to parse relevant information
   * from the posting to better organize and display the information to the user.
   * @param objEvent {Object} event object.  Resolve the transport via the property [object].target
   * @param strId {String}
   */
  gicx.prefetchContentCB = function(objEvent) {
    //resolve which record in the model that the images belong to
    var objHttp = objEvent.target;
    var objMatrix = gicx.APP.getJSX(objHttp.expando_matrix_id);
    var objRecord = objMatrix.getRecordNode(objHttp.expando_parent_id);

    //cache the page content and page header for the posting (no need to go back to craigslist during this session)
    var objFilter = new gicx.Filter(objHttp.getResponseText());
    var ret = objFilter.getBody();
    if(ret && ret.length) {
      objRecord.setAttribute("content",ret[1]);
      objRecord.setAttribute("header",ret[0]);
    }

    //cache the images if found
    if(objRecord.getAttribute("hasCachedImages") != "1") {
      objRecord.setAttribute("hasCachedImages","1");
      objRecord.setAttribute("pic","pic");
      var objImages = objFilter.getImages();

      //loop to add each image found on the given page
      for(var i=0;i<objImages.length  && i < 4;i++) {
        var objImg = objRecord.createNode(jsx3.xml.Entity.TYPEELEMENT,"img");
        objRecord.appendChild(objImg);
        objImg.setAttribute("src",(i == 3)?gicx.APP.resolveURI("images/see_next.gif"):objImages[i]);
      }

      //put the record in the soft cache
      if(!gicx.Cache.getRecord("searchHistory",objHttp.expando_parent_id) && !gicx.Cache.getRecord("_searchHistory",objHttp.expando_parent_id))
        gicx.Cache.setRecord("_searchHistory",objRecord);

      //redraw the cell in the matrix in order to show the cell; throttle the repaint
      objMatrix.redrawMappedCells(objHttp.expando_parent_id,"pic");
      window.clearTimeout(gicx.thumb_timeout);
      gicx.thumb_timeout = window.setTimeout(function()  { if(gicx.APP.getJSXByName("blkxSearchResults").getDisplay() != "none") gicx.APP.getJSXByName("blkxSearchResults").repaint(); },500);
    }
  };


  /**
   * Called when the user clicks on the 'hide images...' context menu item. Hides any images (even if already loaded on the client) for the given record
   * @param objMatrix {jsx3.gui.Matrix}
   * @param strId {String}
   */
  gicx.hideCachedImages = function(objMatrix,strId) {
    objMatrix.insertRecordProperty(strId,"hideCachedImages","1",false);
    objMatrix.redrawMappedCells(strId, "pic");
    objMatrix.synchronizeVScroller();
    window.clearTimeout(gicx.thumb_timeout);
    gicx.thumb_timeout = window.setTimeout(function()  { if(gicx.APP.getJSXByName("blkxSearchResults").getDisplay() != "none") gicx.APP.getJSXByName("blkxSearchResults").repaint(); },500);
  };


  /**
   * Dumbnails an image down to some predefined maximum dimension. This method call is triggered via the onload event for a given HTML img.
   * The image is set to 'visibility:hidden' when it is first requested.  This allows it to load full size, but not be seen.  If it turns out that the image
   * is too large once loaded, its largest dimension will be dubmnailed (since the browser automatically constains the opposing axis, it will likwise resize).
   * @param objImg {HTMLElement} img element
   */
  gicx.doMakeThumb = function(objImg) {
    var intHeight = objImg.offsetHeight;
    var intWidth = objImg.offsetWidth;
    if(intWidth > intHeight) {
      if(intWidth > intDumbnailSize)
        objImg.className = "search_thumb_width";
    } else if(intHeight > intDumbnailSize) {
      objImg.className = "search_thumb_height";
    }

    //expanding images makes the table rows larger; tell the search results matrix to reset it vscroller
    gicx.getResultsTable().synchronizeVScroller();
  };


  /**
   * Updates the CSS style rules that affect the dimensions of the dumbnail images. Called when user moves the slider control.
   * @param fpValue {number} value between 1 and 100.  All images will be dumbnailed twice this value (meaning images get no larger than 200px)
   */
  gicx.scaleImages = function(fpValue) {
    intDumbnailSize = Math.max(20,parseInt(fpValue * 2));
    jsx3.html.updateRule(".search_thumb_width","width",intDumbnailSize + "px");
    jsx3.html.updateRule(".search_thumb_height","height",intDumbnailSize + "px");
    jsx3.html.updateRule(".search_thumb_span","width",(intDumbnailSize+6) + "px");
    jsx3.html.updateRule(".search_thumb_span","height",(intDumbnailSize+6) + "px");
    jsx3.html.updateRule(".search_thumb_span_container","height",(intDumbnailSize+10) + "px");
    jsx3.html.updateRule(".search_thumb_div","width",(intDumbnailSize+6) + "px");
    jsx3.html.updateRule(".search_thumb_div","height",(intDumbnailSize+6) + "px");
    gicx.getResultsTable().synchronizeVScroller();
  };



  /**
   * Returns the search results matrix
   */
  gicx.getResultsTable = function() {
    return gicx.APP.getJSXByName("mtxSearchResults");
  };



  /**
   * Returns the favorites matrix
   */
  gicx.getFavoritesTable = function() {
    return gicx.APP.getJSXByName("mtxFavorites");
  };



  /**
   * Returns the blocked postings matrix
   */
  gicx.getBlockedTable = function() {
    return gicx.APP.getJSXByName("mtxBlocked");
  };


  /**
   * Returns the search history matrix
   */
  gicx.getHistoryTable = function() {
    return gicx.APP.getJSXByName("mtxHistory");
  };


  /**
   * Auto-scrolls the matrix
   * @param bStart {Boolean} If true, start the auto-scroller
   */
  gicx.doAutoScroll = function(bStart) {
    window.clearInterval(gicx.auto_scroll_interval);
    if(bStart) {
      var objM = gicx.getResultsTable();
      gicx.auto_scroll_interval = window.setInterval(function() {
        objM = gicx.getResultsTable();
        objM.setScrollTop(objM.getScrollTop() + 10);
      },100);
    }
  };


  /**
   * Animates fading in and out
   * @param app {HTMLElement} target object
   * @param cur {number} current opacity (0 - 1)
   * @param dec {number} either the decrement or increment (-1 to 1)
   * @param delay {number} milliseconds to pause before reducing/increasing the opacity
   * @param dir {number} whether to increase or decrease the opacity (-1 or 1)
   */
  gicx._setOpacity = function(app,cur,dec,delay,dir) {
    var curStyle = jsx3.html.getCSSOpacity(cur);
    var objStyle = curStyle.split(/\s*:\s*/);
    if (objStyle && objStyle.length == 2) {
      var strStyleName = objStyle[0].replace(/(-\S)/gi,function($0,$1){ return $1.substring(1).toUpperCase(); });
      app.style[strStyleName] = objStyle[1];
    } else {
      return;
    }
    if((cur > 0 && dir < 0) || (dir > 0 && cur < 1)) {
      window.setTimeout(function() { gicx._setOpacity(app,cur+(dec*dir),dec,delay,dir); },delay);
    } else if(dir<0) {
      app.style.visibility = "hidden";
      app.style.zIndex = -1;
    } else {
      //cancel the opacity (100% is not the same as none in IE)
      app.style[strStyleName] = "";
    }
  };



  /**
   * When user clicks the 'Find' link on the contact pullout, tries to locate the
   * address via Google Map interface (refer to gicx.Contact to see how google maps
   * were implemented for this application)
   * @param dAddress {String} The address to locate
   */
  gicx.getPoint = function(dAddress) {
    gicx.Contact.showPostingAddress(dAddress,gicx.Posting.getActive().getName());
  };



  /**
   * When user clicks the 'Route' link on the contact pullout, tries to get driving directions to the
   * address via Google Map interface (refer to gicx.Contact to see how google maps
   * were implemented for this application)
   * @param dAddress {String} The address to locate. Note that the start address is the default address that the user entered into their user preferences field
   */
  gicx.getRoute = function(dAddress) {
    var objDlg = gicx.APP.getBodyBlock().loadAndCache("components/geo_address/google_drive.xml",false);
    objDlg.getDescendantOfName("txtSAddress").setValue(gicx.Cache.getRecord("preferences","streetaddress").getAttribute("value"));
    objDlg.getDescendantOfName("txtDAddress").setValue(dAddress);
    var objAbs = gicx.APP.getBodyBlock().getAbsolutePosition();
    objDlg.setWidth(parseInt(objAbs.W * .75));
    objDlg.setHeight(parseInt(objAbs.H * .75));
    gicx.APP.getBodyBlock().paintChild(objDlg);
    gicx.doGetRoute(objDlg.getDescendantOfName("routingTools"));
  };


  /**
   * Gets the driving directions
   * @param objJSX {objJSX}
   */
  gicx.doGetRoute = function(objJSX) {
    var strHTML = gicx.Contact.getDrivingDirections(objJSX.getDescendantOfName("txtSAddress").getValue(),
                                                    objJSX.getDescendantOfName("txtDAddress").getValue(),
                                                    objJSX.getDescendantOfName("blkDirections"));
  };


  /**
   * Load dialog that allows user to view/update their user preferences
   */
  gicx.updateUserPrefs = function() {
    var objMatrix = gicx.APP.getJSXByName("mtxPreferences");
    if(!objMatrix) {
      var objDlg = gicx.APP.getBodyBlock().loadAndCache("components/user_prefs.xml",false);
      gicx.APP.getBodyBlock().paintChild(objDlg);
    } else {
      objMatrix.getAncestorOfType(jsx3.gui.Dialog).doToggleState(jsx3.gui.Dialog.MAXIMIZED);
    }
  };


  /**
   * Load dialog that allows user to see their favorite posts, blocked posts, and search history
   */
  gicx.viewFavorites = function() {
    var objMatrix = gicx.APP.getJSXByName("mtxFavorites");
    if(!objMatrix) {
      var objDlg = gicx.APP.getBodyBlock().loadAndCache("components/favorites.xml",false);
      gicx.APP.getBodyBlock().paintChild(objDlg);
    } else {
      objMatrix.getAncestorOfType(jsx3.gui.Dialog).doToggleState(jsx3.gui.Dialog.MAXIMIZED);
    }
  };


  /**
   * Load dialog that allows user to create a posting
   */
  gicx.doOpenPage = function(strURL) {
    var objDlg = gicx.APP.getBodyBlock().loadAndCache("components/create_posting.xml",false);
    objDlg.getDescendantsOfType(jsx3.gui.TextBox)[0].setValue(strURL);
    var objAbs = gicx.APP.getBodyBlock().getAbsolutePosition();
    objDlg.setWidth(parseInt(objAbs.W * .75));
    objDlg.setHeight(parseInt(objAbs.H * .75));
    gicx.APP.getBodyBlock().paintChild(objDlg);
    objDlg.getDescendantsOfType(jsx3.gui.IFrame)[0].setSrc(strURL).repaint();
  };


  /**
   * Load dialog that allows user to get driving directions for a start and destination address
   */
  gicx.udpatePrefsExecute = function(objDlg) {
    //called when user hits the save button; saves the xml source for the containing matrix to dojo.storage
    jsx3.log("TO DO: save user prefs to file system");
  };


  /**
   * When view mode is thumbnail (not list), a hover displays when a set of images is hovered over
   */
  gicx.doSpyThumb = function(objElm,objEvent,bShow) {
    //wrap the native event for the common API
    var oE = jsx3.gui.Event.wrap(objEvent);

    //if active, make #4AF1F5
    objElm.style.backgroundColor = bShow ? '#F9DE97' : '';
    objElm.style.borderColor = bShow ? '#FF8000' : '#f0f0f1';
  };


  /**
   * Checks the results table as well as the hard and soft caches for the record; if found, publishes the 'open record' event
   */
  gicx.openPosting = function(strRecordId) {
    var objRecord = gicx.Cache.getRecord("searchHistory",strRecordId);
    if(!objRecord)
      objRecord = gicx.Cache.getRecord("_searchHistory",strRecordId);
    if(!objRecord)
      objRecord = gicx.getResultsTable().getRecordNode(strRecordId);
    if(objRecord) {
      //until state-tracking module is built (basically updates the location in the addressbar), manually set the location
      window.location.href = "#" + strRecordId;
      gicx.Posting.open(objRecord);
    }
  };


  /**
   * Subscribed to the on-favorite event;  repaints the favorites table
   */
  gicx.onFavorite = function(objEvent) {
    if(gicx.getFavoritesTable())
      gicx.getFavoritesTable().repaint();
  };


  /**
   * Subscribed to the on-favorite event;  repaints the favorites table
   */
  gicx.onUnfavorite = function(objEvent) {
    if(gicx.getFavoritesTable())
      gicx.getFavoritesTable().repaint();
  };


  /**
   * Subscribed to the on-block event;  repaints the blocked table
   */
  gicx.onBlock = function(objEvent) {
    if(gicx.getBlockedTable())
      gicx.getBlockedTable().repaint();

    if(objEvent.target instanceof jsx3.xml.Entity)
      gicx.getResultsTable().deleteRecord(objEvent.target.getAttribute("jsxid"));
  };


  /**
   * Subscribed to the on-unblock event;  repaints the blocked table
   */
  gicx.onUnblock = function(objEvent) {
    if(gicx.getBlockedTable())
      gicx.getBlockedTable().repaint();
  };


  /**
   * Subscribed to the on-archive event;  repaints the search archive table
   */
  gicx.onSearchArchive = function(objEvent) {
    if(gicx.getHistoryTable())
      gicx.getHistoryTable().repaint();
  };


  /**
   * Subscribed to the on-unarchive event;  repaints the search archive table
   */
  gicx.onSearchUnarchive = function(objEvent) {
    if(gicx.getHistoryTable())
      gicx.getHistoryTable().repaint();
  };


  /**
   * Persists the user's MRU choices as a cookie
   */
  gicx.setMRUPrefs = function() {
    //get an array of all MRU items
    var a = new Array();
    var objNodes = gicx.APP.getCache().getDocument("regions").selectNodeIterator("//record[@shortcut]");
    while(objNodes.hasNext())
      a.push(objNodes.next().getAttribute("shortcut"));

    //persist the array via user settings class
    var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
    if(a.length) {
      oSettings.set("mru",a);
    } else {
      oSettings.remove("mru");
    }
    oSettings.save();
  };


  /**
   * Restores the user's MRU choices when app first initializes
   */
  gicx.restoreMRUPrefs = function() {
    //restore the user's prior region settings
    jsx3.require("jsx3.app.UserSettings");
    var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
    selectedIds = oSettings.get("mru");

    if(!(selectedIds instanceof Array) || !selectedIds.length)
      return;

    //add the new region selecttions
    var objJSX = gicx.APP.getJSXByName("mnuRegions");
    for(var i=0;i<selectedIds.length;i++)
      gicx.addMRU(objJSX,selectedIds[i]);
  };


  /**
   * Adds a most recently used item to the regions selector to allow users faster access to their most common search regions
   * @param objJSX {jsx3.gui.Menu}
   * @pram strRefId {String} ID of menu item to clone and add to the MRU list
   */
  gicx.addMRU = function(objJSX,strRefId) {
    //add the mru item
    var objMRU = objJSX.getRecordNode("///" + strRefId);
    if(!objMRU) {
      //remove the oldest MRU item
      var objRefRec = objJSX.getRecordNode("/al");
      objJSX.deleteRecord(objRefRec.getPreviousSibling().getAttribute("jsxid"));

      //add the new MRU entry by cloning the user's choice; insert as the first record in the entire document
      var objRec = objJSX.getRecord(strRefId);
      objRec.jsxid = "///" + strRefId;
      objRec.shortcut = strRefId;
      delete objRec.selected;
      delete objRec.jsxdivider;
      objJSX.insertRecordBefore(objRec, objJSX.getXML().getRootNode().getFirstChild().getAttribute("jsxid"));
    }
  };


  /**
   * Called when user chooses a menu item from the regions menu. Adds the selection; advances the stage (if relevant); adds item to the MRU list.
   * @param objEvent {jsx3.gui.Event}
   * @param objJSX {jsx3.gui.Menu}
   * @pram objRecord {jsx3.xml.Entity} what was chosen
   */
  gicx.onChooseFirstRegion = function(objEvent,objJSX,objRecord) {
    //if the user chose a "quick link", resolve to the real menu item
    var strRefId = objRecord.getAttribute("shortcut");
    if(strRefId) {
      objRecord = objRecord.selectSingleNode("//record[@jsxid='" + strRefId + "']");
    } else {
      //add an entry to the MRU list of regions (last four choices at the top)
      gicx.addMRU(objJSX,objRecord.getAttribute("jsxid"));

      //persist the MRU settings to a cookie
      gicx.setMRUPrefs();
    }

    //put flag on record to show it as selected
    objRecord.setAttribute("selected","1");

    //Add a region object
    gicx.setRegionPrefs();

    //animate the next step (fade-in to draw user's attention)
    gicx.showStage2();
  };


  /**
   * Called when user chooses a menu item from the categories menu. Adds the selection; advances the stage (if relevant).
   * @param objEvent {jsx3.gui.Event}
   * @param objJSX {jsx3.gui.Menu}
   * @pram objRecord {jsx3.xml.Entity} what was chosen
   */
  gicx.onChooseFirstCategory = function(objEvent,objJSX,objRecord) {
    //put flag on record to show it as selected
    objRecord.setAttribute("selected","1");

    //Add a region object
    gicx.setCategoryPrefs();

    //animate the next step (fade-in to draw user's attention)
    gicx.showStage3();
  };


  /**
   * Animates the display of step/stage 2 for the first-use use case.  Fades the next step in to cue the user what to click.
   */
  gicx.showStage2 = function() {
    var objReg = gicx.APP.getJSXByName("blkCategories");
    if(objReg.getVisibility() == "hidden") {
      objReg.setVisibility("",true);
      gicx._setOpacity(objReg.getRendered(),.05,.05,80,1);
    } else if(gicx.APP.getJSXByName("blkSplitMain").getVisibility() != "hidden") {
      gicx.search();
    }
  };


  /**
   * Animates the display of step/stage 3 for the first-use use case.  Fades the next step in to cue the user what to click.
   */
  gicx.showStage3 = function() {
    var objCat = gicx.APP.getJSXByName("blkSplitMain");
    if(objCat.getVisibility() == "hidden") {
      objCat.setVisibility("",true);
      gicx._setOpacity(objCat.getRendered(),.05,.05,80,1);
      window.setTimeout(function() { gicx.search(); },1000);
    } else {
      gicx.search();
    }
  };

  /**
   * When the user chooses a region or category, calls  method to create the visual object on-screen to show what they chose in the menu
   * @param objC {jsx3.gui.Block} parent object to contain the visual on-screen object
   * @pram objRecord {jsx3.xml.Entity} what was chosen
   * @param cacheId {String} id of document in XML cache from which the region object was created
   */
  gicx.addSearchObject = function(objC,objRecord,cacheId) {
    var objR = objC.loadAndCache("components/region_object.xml",false);
    objR.setText(objRecord.getAttribute("jsxtext"));
    objR.mappedid = objRecord.getAttribute("jsxid");
    objR.cacheid = cacheId;
    objC.insertBefore(objR,objR.getPreviousSibling());
  };


  /**
   * Removes a given region/category criteria from the search
   * @param objJSX {jsx3.gui.Block} the object to remove
   */
  gicx.removeSearchObject = function(objJSX) {
    //remove selection flag in cache doc and remove on-screen object that user just clicked
    var objRecord = gicx.APP.getCache().getDocument(objJSX.cacheid).selectSingleNode("//record[@jsxid='" + objJSX.mappedid + "']");
    objRecord.removeAttribute("selected");
    if(objRecord.getAttribute("all") == "1")
      objRecord = objRecord.getParent();

    //delete any subselections
    var i = objRecord.selectNodeIterator(".//record[@selected='1']");
    while(i.hasNext())
      i.next().removeAttribute("selected");

    //persist the cookie
    if(objJSX.cacheid == "regions") {
      gicx.setRegionPrefs();
    } else {
      gicx.setCategoryPrefs();
    }

    //requery since the parameter has changed
    gicx.search();
  };


  /**
   * Restores the user's chosen search regions from a cookie
   * @param selectedIds {Array<String>} this is how to manually set this value (as would happen when restoring a search)
   */
  gicx.getRegionPrefs = function(selectedIds) {
    //restore the user's prior region settings
    jsx3.require("jsx3.app.UserSettings");

    if(!(selectedIds instanceof Array) || !selectedIds.length) {
      var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
      selectedIds = oSettings.get("r");
    }

    if(!(selectedIds instanceof Array) || !selectedIds.length)
      return;
      //selectedIds = [gicx.Query.DEFAULT_REGION];
    var objXML = gicx.APP.getCache().getDocument("regions");

    //remove existing region selections
    var objNodes = objXML.selectNodeIterator("//record[@selected='1']");
    while(objNodes.hasNext())
      objNodes.next().removeAttribute("selected");

    //add the new region selecttions
    for(var i=0;i<selectedIds.length;i++) {
      var objNode = objXML.selectSingleNode("//record[@jsxid='" + selectedIds[i] + "']");
      if(objNode)
        objNode.setAttribute("selected","1");
    }
  };


  /**
   * Persists the user's chosen search regions as a cookie
   */
  gicx.setRegionPrefs = function() {
    //get an array of all check rows
    var objNodes = gicx.APP.getCache().getDocument("regions").selectNodeIterator("//record[@selected='1' and not(preceding-sibling::record[@all='1' and @selected='1'] or ../preceding-sibling::record[@all='1' and @selected='1'])]");
    var a = new Array();
    while (objNodes.hasNext())
      a.push(objNodes.next().getAttribute("jsxid"));

    //persist the array viar user settings class
    var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
    if(a.length == 0) a.push("nullvalue");
    if(true || a.length) {
      oSettings.set("r",a);
    } else {
      oSettings.remove("r");
    }
    oSettings.save();
    gicx.showRegionPrefs();
  };


  /**
   * Paints a list of all regions that will be searched, so the user has a visual indication of where they're searching
   */
  gicx.showRegionPrefs = function() {
    //query the user settings for which regions to search; if a region is selected, ignore the sub-regions that belong to it (just search the region)
    var objXMLR = gicx.APP.getCache().getDocument("regions");
    objXMLR.setSelectionLanguage("XPath");
    var objNodes = objXMLR.selectNodeIterator("//record[@selected='1' and not(preceding-sibling::record[@all='1' and @selected='1'] or ../preceding-sibling::record[@all='1' and @selected='1'])]");
    var a = [];
    var objParent = gicx.APP.getJSXByName("blkRegions");
    for(var i=objParent.getChildren().length-2;i>=0;i--)
      objParent.removeChild(i);
    while (objNodes.hasNext()) {
      var objNode = objNodes.next();
      gicx.addSearchObject(objParent,objNode,"regions");
    }
  };


  /**
   * Restores the user's chosen search categories from a cookie; if not value, the default category of 'all for sale' will be used
   * @param selectedIds {Array<String>} this is how to manually set this value--override the cookie
   */
  gicx.getCategoryPrefs = function(selectedIds) {
    //restore the user's prior region settings
    jsx3.require("jsx3.app.UserSettings");

    if(!(selectedIds instanceof Array) || !selectedIds.length) {
      var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
      var selectedIds = oSettings.get("c");
    }

    //if not found in the cookie, default to 'all for sale' (sss)
    if(!(selectedIds instanceof Array) || !selectedIds.length)
      return;
      //selectedIds = [gicx.Query.DEFAULT_CATEGORY];
    var objXML = gicx.APP.getCache().getDocument("categories");

    //remove existing region selections
    var objNodes = objXML.selectNodeIterator("//record[@selected='1']");
    while(objNodes.hasNext())
      objNodes.next().removeAttribute("selected");

    //add the new region selecttions
    for(var i=0;i<selectedIds.length;i++) {
      var objNode = objXML.selectSingleNode("//record[@jsxid='" + selectedIds[i] + "']");
      if(objNode)
        objNode.setAttribute("selected","1");
    }
  };


  /**
   * Persists the user's chosen search categories to a cookie
   */
  gicx.setCategoryPrefs = function() {
    //get an array of all check rows
    var objNodes = gicx.APP.getCache().getDocument("categories").selectNodeIterator("//record[@selected='1' and not(preceding-sibling::record[@all='1' and @selected='1'] or ../preceding-sibling::record[@all='1' and @selected='1'])]");
    var a = new Array();
    while (objNodes.hasNext())
      a.push(objNodes.next().getAttribute("jsxid"));

    //persist the array viar user settings class
    var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
    if(a.length == 0) a.push("nullvalue");
    if(true || a.length) {
      oSettings.set("c",a);
    } else {
      oSettings.remove("c");
    }
    oSettings.save();
    gicx.showCategoryPrefs();
  };


  /**
   * Paints a list of all categories that will be searched, so the user has a visual indication of where they're searching
   * Calls method that displays the search fields appropriate to the chosen category
   * @see gicx.provideSearchFields
   */
  gicx.showCategoryPrefs = function() {
    //query the user settings for which regions to search; if a region is selected, ignore the sub-regions that belong to it (just search the region)
    var objXMLC = gicx.APP.getCache().getDocument("categories");
    objXMLC.setSelectionLanguage("XPath");
    var objNodes = objXMLC.selectNodeIterator("//record[@selected='1' and not(preceding-sibling::record[@all='1' and @selected='1'] or ../preceding-sibling::record[@all='1' and @selected='1'])]");
    var b = [];
    var objParent = gicx.APP.getJSXByName("blkCategories");
    for(var i=objParent.getChildren().length-2;i>=0;i--)
      objParent.removeChild(i);
    while (objNodes.hasNext()) {
      var objNode = objNodes.next();
      gicx.addSearchObject(objParent,objNode,"categories");
      var intField = objNode.getAttribute("fieldflag");
      b.push((!jsx3.util.strEmpty(intField)?intField:objNode.getParent().getAttribute("fieldflag"))-0);
    }
    gicx.provideSearchFields(b);
  };



  /**
   * Updates which search input fields to display appropriate to the categories that the user selected
   * @param objArray {Array} Array of flags
   */
  gicx.provideSearchFields = function(objArray) {
    //open document as a CDF document instance (not just XML--since easier to query using CDF syntax)
    var objXML = gicx.APP.getCache().getDocument("searchFields");
    if(!objXML) {
      var objXML = jsx3.xml.CDF.Document.newDocument().load(gicx.APP.resolveURI("xml/searchFields.xml"));
      gicx.APP.getCache().setDocument("searchFields",objXML);
    }

    //get container that holds the dynamic fields--remove them
    var objParent = gicx.APP.getJSXByName("blkDynamicFields");
    objParent.removeChildren();

    for(var i=0;i<objArray.length;i++) {
      var intMaxFlag = 32768;
      var intFlag = objArray[i];
      do {
        if(intMaxFlag <= intFlag) {
          if(intMaxFlag == 1024) continue;
          var objRec = objXML.getRecord(intMaxFlag);
          if(objRec) {
            var objJSX = gicx.APP.getJSXByName(objRec.name);
            if(!objJSX) {
              if(objRec.type == "ImageButton") {
                var objJSX = objParent.loadAndCache("components/searchFields/ImageButton.xml",false);
                objJSX.setWidth(objRec.width);
                objJSX.setState(objRec.value-0);
                objJSX.setTip(objRec.tip);
                objJSX.setName(objRec.name);
                objJSX.setImage(objRec.img_off);
                objJSX.setOnImage(objRec.img_on);
                objJSX.truevalue = objRec.truevalue;
                objJSX.truename = objRec.truename;
                objParent.paintChild(objJSX);
              } else {
                var objJSX = objParent.loadAndCache("components/searchFields/TextBox.xml",false);
                objJSX.defaultlabeltext = objRec.label
                objJSX.setValue(objRec.label);
                objJSX.setName(objRec.name);
                objJSX.setTip(objRec.tip);
                objParent.paintChild(objJSX);
              }
            } else {
              jsx3.log('what??' + intMaxFlag);
            }
          }
          intFlag-= intMaxFlag;
        }
        intMaxFlag/=2;
      } while(intMaxFlag >=1);

    }
  };


  /**
   * Persists 'user preferences' from the xml data model used by the app to a cookie
   */
  gicx.setUserPrefs = function() {
    //get an array of all user prefs.
    var objNodes = gicx.APP.getCache().getDocument("preferences").selectNodeIterator("//record");
    var a = new Array();
    while (objNodes.hasNext()) {
      var objNode = objNodes.next();
      a.push(objNode.getAttribute("jsxid"));
      a.push(objNode.getAttribute("value"));
    }

    //persist the array via user settings class
    var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
    oSettings.set("p",a);
    oSettings.save();
  };


  /**
   * Restores the 'user preferences' from a cookie to the xml data model used by the application
   */
  gicx.getUserPrefs = function() {
    //restore the user's prior region settings
    jsx3.require("jsx3.app.UserSettings");
    var oSettings = new jsx3.app.UserSettings(gicx.APP,jsx3.app.UserSettings.PERSIST_INDEFINITE);
    var a = oSettings.get("p");
    if(!(a instanceof Array) || !a.length)
      return;

    //restore the user settings (in JavaScript array format) to the CDF model format
    var objXML = gicx.APP.getCache().getDocument("preferences");
    for(var i=0;i<a.length-1;i++) {
      var objNode = objXML.selectSingleNode("//record[@jsxid='" + a[i] + "']");
      if(objNode)
        objNode.setAttribute("value",a[i+1]);
    }
  };

});




/**
 * Provides support for contacting craigslist and querying for a given posting, index page, search results page,
 * or email-to-friend page.  Allows developer to construct a paramaterized query and then send using a
 * jsx3.net.Request instance.  Also provides support for contacting the CL RSS service, using Yahoo!pipes as
 * the cross-domain transport.
 * @author birdeau@yahoo.com
 */
jsx3.lang.Class.defineClass("gicx.Query", jsx3.net.Request, [], function(Query,Query_prototype) {

  var queryInc = 0;

  Query.DEFAULT_SCHEME = "http://";
  Query.DEFAULT_REGION = "sfbay";
  Query.DEFAULT_HOST = "craigslist.org";
  Query.DEFAULT_SUBREGION = "sfc";
  Query.DEFAULT_CATEGORY = "sss";
  Query.DEFAULT_PAGE_SIZE = 100;
  Query.DEFAULT_FRIEND_FORM = {postingID:"",action:"send_email",type:"",email:"",rcptAddress:""};
  Query.PIPES_SEARCH_ID = "Wr08WNL42xG8NqN5jknRlg";  //this is the unique id given to the pipe that I created for search CL for RSS feeds

  Query.ADD = "addTwo";     //by-owner, by-dealer
  Query.PIC = "hasPic";     //1
  Query.MIN = "minAsk";     //\d*
  Query.MAX = "maxAsk";     //\d*
  Query.TITLE = "srchType"; //T
  Query.QUERY = "query";    //[\s\S]*
  Query.START = "s";


  Query_prototype.init = function(strId) {
    this.jsxsuper(strId);
    this._params = {addTwo:null,hasPic:null,minAsk:null,maxAsk:null,srchType:null,query:null,s:0};
  };


  Query_prototype.getProxyPrepend = function(strId) {
    //my HTTP server proxies the request to avoid x-domain security errors.  This prepend tells my server to proxy the request
    return (!jsx3.util.strEmpty(gicx.APP.getEnv("proxy"))) ? gicx.APP.getEnv("proxy") : "";
  };


  //composites the URL appropriate for executing a search for posting
  Query_prototype.getFriendURL = function() {
    //resolve the query parameters
    var a = [];
    Query.DEFAULT_FRIEND_FORM.postingID = this.postingid;
    for(var p in Query.DEFAULT_FRIEND_FORM)
      a.push(p + "=" + Query.DEFAULT_FRIEND_FORM[p]);

    //resolve the url
    var strURL = this.getScheme(gicx.Query.DEFAULT_SCHEME) +
                 this.getRegion(gicx.Query.DEFAULT_REGION) + "." +
                 this.getHost(gicx.Query.DEFAULT_HOST) + "/email.friend?" +
                 a.join("&");

    //return
    jsx3.log("Emailing Friend: " + this.getProxyPrepend() + strURL);
    return this.getProxyPrepend() + strURL;
  };


  //composites the URL appropriate for executing a search for postings
  Query_prototype.getSearchURL = function() {
    //special case: user can't search in titles if no query string; force the equivalent redirect that cl would issue
    if(this.getParam(Query.TITLE) && jsx3.util.strEmpty(this.getParam(Query.QUERY)))
      this.removeParam(Query.TITLE);
    var strParams = this._getEncodedParameters();
    if(strParams) {
      var strURL = this.getScheme(gicx.Query.DEFAULT_SCHEME) +
                   this.getRegion(gicx.Query.DEFAULT_REGION) + "." +
                   this.getHost(gicx.Query.DEFAULT_HOST) + "/search/" +
                   this.getCategory(gicx.Query.DEFAULT_CATEGORY) +
                   ((this.getSubregion()) ? "/" + this.getSubregion() : "") +
                   (strParams ? "?" + strParams : "");
    } else {
      //queries with no parameters directly index the list server in a paginated model
      if(!this.indexquery)
        this.indexquery = 0;

      var strURL = this.getScheme(gicx.Query.DEFAULT_SCHEME) +
                   this.getRegion(gicx.Query.DEFAULT_REGION) + "." +
                   this.getHost(gicx.Query.DEFAULT_HOST) +
                   ((this.getSubregion()) ? "/" + this.getSubregion() : "") + "/" +
                   this.getCategory(gicx.Query.DEFAULT_CATEGORY) + "/";

      //if greater than '0', add the index to search on the list
      if(this.indexquery)
        strURL += "index" + this.indexquery + ".html";
    }
    jsx3.log("Search URL: " + this.getProxyPrepend() + strURL);

    return this.getProxyPrepend() + strURL;
  };


  //composites the URL needed to return a specific posting
  Query_prototype.getPageURL = function() {
    var strURL = this.url;
    jsx3.log("Opening Posting: " + this.getProxyPrepend() + strURL);
    return this.getProxyPrepend() + strURL;
  };


  Query_prototype.getScheme = function(strDefault) {
    return this.scheme || strDefault;
  };

  Query_prototype.setScheme = function(strScheme) {
    this.scheme = strScheme;
  };

  Query_prototype.getRegion = function(strDefault) {
    return this.region || strDefault;
  };

  Query_prototype.setRegion = function(strRegion) {
    this.region = strRegion;
  };

  Query_prototype.getHost = function(strDefault) {
    return this.host || strDefault;
  };

  Query_prototype.setHost = function(strHost) {
    this.host = strHost;
  };

  Query_prototype.getSubregion = function() {
    return this.subregion;
  };

  Query_prototype.setSubregion = function(strSubregion) {
    this.subregion = strSubregion;
  };

  Query_prototype.getCategory = function(strDefault) {
    return this.category || strDefault;
  };

  Query_prototype.setCategory = function(strCategory) {
    this.category = strCategory;
  };

  Query_prototype.setURL = function(strURL) {
    this.url = strURL;
  };

  Query_prototype.getURL = function() {
    return this.url;
  };

  Query_prototype.setPostingId = function(intId) {
    this.postingid = intId;
  };

  Query_prototype.getPostingId = function() {
    return this.postingid;
  };

  Query_prototype.setParam = function(strName,strValue) {
    this._params[strName] = strValue;
  };

  Query_prototype.getParam = function(strName) {
    return this._params[strName];
  };

  Query_prototype.removeParam = function(strName) {
    delete this._params[strName];
  };

  Query_prototype.getParams = function(strCategory) {
    return this._params;
  };


  //returns URI-encoded parameter set for a given query
  Query_prototype._getEncodedParameters = function() {
    var a = [];
    for(var p in this._params)
      if(!(this._params[p] == null || (p == Query.START && this._params[p] == 0)))
        a.push(p + "=" + window.encodeURIComponent(this._params[p]));
    return a.join("&");
  };


  //method that wraps the open methosd on the super; emails the posting to a given email address
  Query_prototype.openForFriend = function(bAsync,strUserName,strUserPass) {
    //TO DO: use arguments object to generically persist these
    this._user = strUserName;
    this._pass = strUserPass;
    this._async = bAsync;
    this.open("get",this.getFriendURL(),bAsync,strUserName,strUserPass);
  };


  //method that wraps the open method on the super; gets a list of search results
  Query_prototype.openForSearch = function(bAsync,strUserName,strUserPass) {
    //TO DO: use arguments object to generically persist these
    this._user = strUserName;
    this._pass = strUserPass;
    this._async = bAsync;
    this.open("get",this.getSearchURL(),bAsync,strUserName,strUserPass);
  };


  Query_prototype.openForNext = function() {
    if(this.hasNext()) {
      if(!isNaN(this.indexquery)) {
        this.indexquery = this.indexquery + 100;
      } else {
        this.setParam(Query.START,this.getParam(Query.START) + Query.DEFAULT_PAGE_SIZE);
      }
      this.openForSearch(this._async,this._user,this._pass);
      return true;
    }
    return false;
  };


  Query_prototype.openForPrevious = function() {
    if(this.hasPrevious()) {
      if(!isNaN(this.indexquery)) {
        this.indexquery = this.indexquery - 100;
      } else {
        this.setParam(Query.START,this.getParam(Query.START) - Query.DEFAULT_PAGE_SIZE);
      }
      this.openForSearch(this._async,this._user,this._pass);
      return true;
    }
    return false;
  };


  //method that wraps the open method on the super; gets the specific search results page
  Query_prototype.openForPage = function(bAsync,strUserName,strUserPass) {
    var strPageUrl = this.getPageURL();
    if(!jsx3.util.strEmpty(strPageUrl)) {
      this._user = strUserName;
      this._pass = strUserPass;
      this._async = bAsync;
      this.open("get",strPageUrl,bAsync,strUserName,strUserPass);
      return true;
    }
    return false;
  };


  /**
   * Returns true if there are additional, paginated records to return AFTER the paginated set currently being vieweed
   * @returns {Boolean}
   */
  Query_prototype.hasNext = function() {
    if(!isNaN(this.indexquery))
      return true;
    var objRange = this.getRange();
    return objRange.end < objRange.total;
  };


  /**
   * Returns true if there are additional, paginated records to return PRIOR to the paginated set currently being viewed
   * @returns {Boolean}
   */
  Query_prototype.hasPrevious = function() {
    if(!isNaN(this.indexquery) && this.indexquery >= 100)
      return true;
    return this.getRange().start > 1
  };


  Query_prototype.getRange = function() {
    return (new gicx.Filter(this.getResponseText())).getRange();
  }


  /**
   * Returns the query profile, serialized as a CDF Record. This record can then be used to restore the query at a later time.
   * NOTE: do not serialize a query until it has run.
   * @param bSent {Boolean} pass false if the message has not been sent (don't feel like using a try catch)
   * @returns {jsx3.xml.Entity} CDF Record object
   */
  Query_prototype.serialize = function(bSent) {
    //create the base profile object
    var objParams = {scheme:this.getScheme(Query.DEFAULT_SCHEME),region:this.getRegion(Query.DEFAULT_REGION),host:this.getHost(Query.DEFAULT_HOST),category:this.getCategory(Query.DEFAULT_CATEGORY)};

    //add optional information
    if(this.subregion)
      objParams.subregion = this.subregion;

    //clone relevant query parameters; never save the page index
    for(var p in this._params)
      if(this._params[p])
        objParams[p] = this._params[p];
    delete objParams[Query.START];

    objParams.jsxid = (new Date()).valueOf();
    if(this.url)
      objParams.url = this.url;
    objParams.datetime = (new Date()).valueOf();

    var objDoc = jsx3.xml.CDF.Document.newDocument();
    return objDoc.insertRecord(objParams);
  };


  /**
   * Instances the query, by restoring a serialized instance
   * @param objRecord {jsx3.xml.Entity | strRecord} either an Entity or the record as string
   * @returns {gicx.Query}
   */
  Query.newFromRecord = function(objRecord) {
    //validate the input
    if(typeof(objRecord) == "string") {
      var objDoc = new jsx3.xml.CDF.Document.newDocument();
      objDoc.loadXML('<data jsxid="jsxroot">' + objRecord + '</data>');
      if(!objDoc.hasError()) {
        objRecord = objDoc.selectSingleNode("//record");
        if(!objRecord)
          throw "Invalid input type. Does not adhere to CDF format.";
      } else {
        throw "Invalid input type. Malformed XML.";
      }
    }
    if(!(objRecord instanceof jsx3.xml.Entity && objRecord.getNodeName() == "record"))
      throw "Invalid input type. Either not an instance of jsx3.xml.Entity or nodeName is not 'record'.";

    //create the instance
    var objNew = new gicx.Query("qx" + queryInc++);
    var objAtts = objRecord.getAttributes();
    for(var i=0;i<objAtts.size();i++) {
      var objAtt = objAtts.get(i);
      var strName = objAtt.getNodeName();
      var strValue = objAtt.getValue();
      if(strName == "url" || strName == "scheme" || strName == "region" || strName == "subregion" || strName == "host" || strName == "category") {
        objNew[strName] = strValue;
      } else if(strName != "jsxid" && strName != "count") {
        objNew.setParam(strName,strValue);
      }
    }
    return objNew;
  };


  /**
   * Sends the query via Yahoo pipes.  Enables safe x-domain searching without proxying on my host server.  Not as good as screen-scraping
   * @param objRecord {jsx3.xml.Entity | strRecord} either an Entity or the record as string
   * @returns {gicx.Query}
   */
  Query_prototype.sendWithPipes = function() {
    //create a new cb handler
    var strFnName = "cb" + ((new Date()).valueOf()) + queryInc++;
    this.pipes = true;
    var my = this;
    window[strFnName] = function(objJSON) {
      //modify the query object so that calling getREsponseText returns the JSON-formatted RSS feed
      my.getResponseText = function() { return objJSON; }
      my.publish({subject:jsx3.net.Request.EVENT_ON_RESPONSE});
      //dereference the dynamically created CB function now that no longer needed
      window[strFnName] = null;
    }

    //insert a script tag into the DOM; the code that yahoo pipes returns will call the dynamically-created cb which will call then call the registered client cb and finally unload (destroy) itself
    var strURL = "http://pipes.yahoo.com/pipes/pipe.run?clURL=" + window.encodeURIComponent(this.getSearchURL() + "&format=rss") + "&_id=" + Query.PIPES_SEARCH_ID + "&_run=1&_render=json&_callback=" + strFnName;
    gicx.APP.loadInclude(strURL, "script" + ((new Date()).valueOf()), "script", false);
  };

});




/**
 * Application performance and usability is enhanced by using various caching strategies.  A hard cache is used
 * when storing postings and notes across browser sessions.  A soft cache is used for a single browser session.
 * The hard cache is supported via the dojo storage widget (dojo.storage).  This widget uses the flash plugin,
 * so if a browser does not allow for flash, then a hard cache will not be provided.
 * @author birdeau@yahoo.com
 */
jsx3.lang.Class.defineClass("gicx.Cache", null, [], function(Cache,Cache_prototype) {


  Cache.serialize_throttle;
  Cache.TIMEOUT = 5000;


  /**
   * main method
   */
  Cache_prototype.init = function() {
    this.jsxsuper();
  };


  /**
   * If true, dojo storage is enabled and the hard cache should be activated
   */
  Cache.isEnabled = function() {
    return window.dojo && window.dojo.storage;;
  };


  /**
   * Gets the named document. First tries the in-memory cache. Next tries to restore
   * the document from the dojo storage widget. Finally just instances a new CDF document
   * @param TYPE {jsx3.xml.CDF.Document}
   */
  Cache.getDocument = function(TYPE) {
    var objXML = gicx.APP.getCache().getDocument(TYPE) || Cache.deserialize(TYPE);
    if(jsx3.util.strEmpty(objXML) || !(objXML instanceof jsx3.xml.Document) || objXML.hasError()) {
      var objXML = jsx3.xml.CDF.Document.newDocument();
      gicx.APP.getCache().setDocument(TYPE,objXML);
    }
    return objXML;
  };


  /**
   * Gets a CDF record from a given cache document
   * @param TYPE {String} one of: searchHistory (hard cache), _searchHistory (soft cache)
   * @param strJsxId {String} CDF jsxid
   */
  Cache.getRecord = function(TYPE,strJsxId) {
    var objXML = Cache.getDocument(TYPE);
    return objXML.selectSingleNode("//record[@jsxid='" + strJsxId + "']");
  };


  /**
   * Persists the given CDF record to one of the cacheable CDF documents used by the app (defined by TYPE)
   * @param TYPE {String} one of: searchHistory (hard cache), _searchHistory (soft cache)
   * @param objRN {jsx3.xml.Entity} CDF jsxid
   * @param objPRN {jsx3.xml.Entity} if provided, the new record will be appended to this node;
   * NOTE: objPRN must already be a member of the named document, TYPE
   */
  Cache.setRecord = function(TYPE,objRN,objPRN) {
    //check the cache for the posting
    var objExist = Cache.getRecord(TYPE,objRN.getAttribute("jsxid"));
    if(objExist && objPRN && objExist.getRootNode().equals(objRN.getRootNode())) {
      //this is a move, because objRN is already part of the given DOM;
      //this will support a folder API where favorites can be dragged/dropped in different favorites folders
      objRN.removeAttribute("jsxselected");
      objPRN.appendChild(objRN);
    } else {
      //clone for now; always assume that the original is untouched
      objClone = objRN.cloneNode(true);
      objClone.removeAttribute("jsxselected");

      if(objExist) {
        //replace the existing node with a clone of what was asked to be persisted
        objExist.getParent().replaceNode(objClone,objExist);
      } else {
        //append the clone; this record is not yet part of the persistent store
        if(!objPRN) objPRN = Cache.getDocument(TYPE).getRootNode();
        objPRN.appendChild(objClone);
      }
      var objRN = objClone;
    }

    //cache if not cached (only cache no more frequently than once every 30 seconds)
    window.clearTimeout(Cache.serialize_throttle);
    Cache.serialize_throttle = window.setTimeout(function() { Cache.serialize(TYPE); },Cache.TIMEOUT);

    return objRN;
  };


  /**
   * Deletes the CDF record from one of the cacheable CDF documents used by the app (defined by TYPE)
   * @param TYPE {String} one of: searchHistory (hard cache), _searchHistory (soft cache)
   * @param strId {String} CDF jsxid
   */
  Cache.deleteRecord = function(TYPE,strId) {
    //check the cache for the posting
    var objRN = Cache.getRecord(TYPE,strId);
    if(objRN) {
      objRN.getParent().removeChild(objRN);
      //cache if not cached (only cache no more frequently than once every 30 seconds)
      window.clearTimeout(Cache.serialize_throttle);
      Cache.serialize_throttle = window.setTimeout(function() { Cache.serialize(TYPE); },Cache.TIMEOUT);
      return objRN;
    }
  };


  /**
   * Persists the given xml document to the permanant data store; for now only the hard cache (searchHistory) is persisted
   * @param TYPE {String} one of: searchHistory (hard cache)
   */
  Cache.serialize = function(TYPE) {
    if(Cache.isEnabled())
      dojo.storage.put(TYPE,Cache.getDocument(TYPE).serialize(true));
  };


  /**
   * Returns a persisted document from the local persistent store (dojo.storage)
   * @param TYPE {String} one of: searchHistory (hard cache)
   */
  Cache.deserialize = function(TYPE) {
    //jsx3.log("to do: restore from dojo storage");
    if(typeof(window.dojo) != "undefined" && dojo.storage) {
      var strXML = dojo.storage.get(TYPE);
      if(!jsx3.util.strEmpty(strXML)) {
        var objXML = jsx3.xml.CDF.Document.newDocument();
        objXML.loadXML(strXML);
        if(objXML.hasError()) {
          jsx3.log("Error restoring the stored document, " + TYPE + ". Description: " + objXML.getError().description);
        } else {
          jsx3.log("Restoring the document, '" + TYPE + "', from dojo storage (string) to GI (CDF).");
          gicx.APP.getCache().setDocument(TYPE,objXML);
          return objXML;
        }
      }
    }
  };

});



/**
 * Provides ability to convert a given craigslist HTML page to structured data (XML, JavaScript Array, etc).  Basically scrapes
 * the content and organizes into useful structures.  Capable of understanding posting, index, and search results pages.
 * Also provides support for converting a JSON-fomatted RSS feed routed through Yahoo!pipes into a CDF document.
 * @author birdeau@yahoo.com
 */
jsx3.lang.Class.defineClass("gicx.Filter", null, [], function(Filter,Filter_prototype) {

  //regular expressions used to parse the Craigslist content
  var myListReg = /<p>(?:&nbsp;([^<]*)&nbsp;&nbsp;&nbsp;)?<a href="([^"]*)"[^>]*>([^<]*)<\/a>[\s]*(?:<font[^>]*>[\s]*\(([^<]*)\)<\/font>)?[\s]*(?:<span[^>]*>[\s]*(pic)[^<]*<\/span>)?/ig;
  var myNormalizerReg = /(href|src)=([^"])([^\s\/>]*)(\s|>|\/)/gi;
  var myTargetReg = /href="([^"]*)"/i;
  var myImgReg = /<img[^>]*src="([^"]*\.jpg)"/gi;
  var myBodyReg = /<body[^>]*>([\s\S]*?)(<h2[\s\S]*)<\/body>/i;
  var myCountReg = /<b>Found:[\s]*([\d]*)[\s]*Displaying:[\s]*([,\d]*)[\s]*-[\s]*([,\d]*)<\/b>/i;
  var myEmailReg = /([a-zA-Z0-9_\.+-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,6})/g;
  var myPhoneReg = /(?:[\(]?([0-9]{3})[\)]?[\.| |\-]{0,1}|^[0-9]{3}[\.|\-| ]?)?([0-9]{3})(?:\.|\-| )?([0-9]{4})(( |-|\.)?([ext\.]+ ?\d+))?\b/gi;
  var myPriceReg = /\$(\d*)/;
  var myUrlStripperReg = /http[s]?:[^\s]*/g;
  var myTagStripperReg = /<[^>]*\/?>/g;
  var myEntityReg = /&#(\d*);/gi;
  var myUniStripperReg = /\S?/g;
  var myIndexPageCountReg = /next 100 postings/i;
  var myDateReg = /([^<]*)<\/h4>/i;
  var myGeoReg = /href="http:\/\/maps.google.com\/\?q=loc%3A([^>]*)">/;
  var myRSSLocReg = /(?:\()([^\(^\)]*)(?:\))/;
  var myRSSPriceReg = /(?:\$)(\d*)/;


  /**
   * main method
   */
  Filter_prototype.init = function(strHTML) {
    this.jsxsuper();
    this._html = strHTML;
  };


  /**
   * Returns the innerHTML content of the document body.  Allows the given page from CL to be inlined in the existing web page without need for an IFRAME container.
   * Ensures all hyperlinks in the posting are modified, so they open a new browser window when clicked (target = new)
   * @returns {Array} [header,content]
   */
  Filter_prototype.getBody = function() {
    myBodyReg.exec(this._html);
    var str = RegExp.$1;
    var str2 = RegExp.$2;
    //jsx3.log(str);
    return (str) ? [str.replace(/<a href="\//gi,'<a href="http://www.craigslist.org/').replace(/<a href=/gi,'<a target="_new" href='),str2.replace(/<a href="\//gi,'<a href="http://www.craigslist.org/').replace(/<a href=/gi,'<a target="_new" href=').replace(/<form action="http:\/\/flag/gi,'<form target="_new" action="http://flag')] : null;
  };


  /**
   * Returns an array of URLs for all images on the page. Currently only jpg images are returned.
   * @returns {Array<String>}
   */
  Filter_prototype.getImages = function() {
    //normalize the HTML before scanning for images
    var strHTML = this._html;
    strHTML = strHTML.replace(myNormalizerReg,'$1="$2$3"$4');
    var a = [];
    while(myImgReg.exec(strHTML))
      a.push(RegExp.$1);
    return a;
  };


  /**
   * Returns a single address as set by the poster (a valid google address)
   * @returns {String || null}
   */
  Filter_prototype.getAddress = function() {
    if(typeof(this._html) == "string") {
      this._html.search(myGeoReg);
      var s = RegExp.$1;
      return (jsx3.util.strEmpty(s) || s.length < 5) ? null : s;
    }
  };


  /**
   * Returns an array of emails numbers in a given posting. Never returns more than 4
   * @returns {Array<String>}
   */
  Filter_prototype.getEmails = function() {
    //strip the HTML tags, so only the innerText property exists; also expand entities (craigslist escapes the characters--probably to hide from malicious page scanners)
    var strHTML = this._html;
    strHTML = strHTML.replace(myUniStripperReg,function(a) {return a.charCodeAt(0) > 128 ? " " : a;}).replace(myTagStripperReg," ").replace(myEntityReg,function($1,$2){return String.fromCharCode($2);}).replace(myUrlStripperReg,"").replace(/([\s]*)\s/g," ");
    var a = [];
    var arr;
    var i = 0;
    while((arr = myEmailReg.exec(strHTML)) != null && i<4) {
      a.push([arr[0]]);
      i++;
    }
    return a;
  };


  /**
   * Returns an array of phone/fax numbers in a given posting.
   * @returns {Array<String>}
   */
  Filter_prototype.getPhones = function() {
    //strip the HTML tags, so only the innerText property exists
    var strHTML = this._html;
    strHTML = strHTML.replace(myUniStripperReg,function(a) {return a.charCodeAt(0) > 128 ? " " : a;}).replace(myTagStripperReg," ").replace(myEntityReg,function($1,$2){return String.fromCharCode($2);}).replace(myUrlStripperReg,"").replace(/([\s]*)\s/g," ");
    var a = [];
    var arr;
    var i = 0;
    while((arr = myPhoneReg.exec(strHTML)) != null && i<5) {
      a.push(((RegExp.$1)?"("+RegExp.$1+") ":"") + RegExp.$2 + "-" + RegExp.$3);
      i++;
    }
    return a;
  };


  /**
   * Returns a CDF document of result items, with named record attributes (jsxid, jsxtext, loc, pic);
   * Those postings that are in the 'blocked' list will not be added (even though returned from craigslist)
   * @param objDoc {jsx3.xml.Document} if passed, the records will be added to this document; otherwise a new document will be created
   * @returns {jsx3.xml.CDF.Document}
   */
  Filter_prototype.getPostings = function(objDoc) {
    //convert Craigslist list format to CDF format
    var vntResponse = this._html;
    var objDoc = (objDoc instanceof jsx3.xml.Document) ? objDoc : jsx3.xml.CDF.Document.newDocument();

    //the filter class will handle/convert both RSS (as JSON via yahoo pipes) or a standard search results page (HTML) returned directly from craigslist
    if(typeof(vntResponse) == "string") {
      //h4 delimiters are used to tag dates on all pages rendered in 'list server' mode. split on this in order to get a date for those following
      //records that do not contain a date
      var objHTML = vntResponse.split("<h4>");
      for(var i=0;i<objHTML.length;i++) {
        var vntResponse = objHTML[i];
        myDateReg.exec(vntResponse);
        var s1 = RegExp.$1;

        while(myListReg.exec(vntResponse)) {
          var s2 = RegExp.$1;
          sDate = jsx3.util.strEmpty(s2) ? (s1 || "") : s2;
          var s3 = RegExp.$5;
          s3 = jsx3.util.strEmpty(s3) ? "" : "pic";
          var o = {jsxid:RegExp.$2,jsxtext:RegExp.$3,loc:RegExp.$4,pic:s3,date:sDate};
          //check the permanant and temp caches for the posting; skip the iteration if the given posting should be blocked
          var objNode = gicx.Cache.getRecord("searchHistory",o.jsxid);
          if(objNode && objNode.getParent().getNodeName() == "blocked") {
            jsx3.log("Did not load the blocked posting: " + o.jsxid);
            continue;
          } else if(!objNode) {
            objNode =  gicx.Cache.getRecord("_searchHistory",o.jsxid);
          }

          if(objNode) {
            var objClone = objNode.cloneNode(true);
            objClone.removeAttribute("jsxselected");
            objNode = objDoc.insertRecordNode(objClone);
          } else {
            myPriceReg.exec(o.jsxtext);
            if(!jsx3.util.strEmpty(RegExp.$1))
              o.price = RegExp.$1;
            objNode = objDoc.insertRecord(o);
          }
        }
      }
    } else {
      //filter the response (json-formatted rss)
      if(vntResponse && vntResponse.value && vntResponse.value.items && vntResponse.value.items.length) {
        for(var i=0;i<vntResponse.value.items.length;i++) {
          var curItem = vntResponse.value.items[i];
          var o = {};
          o.jsxid = curItem.link
          o.jsxtext = curItem.title
          o.content = curItem.description;
          o.pic = "";
          var objDate = (curItem.pubDate+"").split("T");
          if(objDate.length == 2) {
            objDate = objDate[0].split("-");
            if(objDate.length == 3)
              o.date = objDate[1] + "-" + objDate[2];
          }
          var sPrice = myRSSPriceReg.exec(o.jsxtext);
          if(!jsx3.util.strEmpty(sPrice))
            o.price = sPrice[1];
            //jsx3.log(sPrice);
          var sLoc = myRSSLocReg.exec(o.jsxtext);
          if(!jsx3.util.strEmpty(sLoc))
            o.loc = sLoc[1];
          objDoc.insertRecord(o);
        }
      }
    }
    return objDoc;
  };


  /**
   * Returns the total number of search results for a given query. This is different from
   * the number of result items returned in the given page and instead is the count of
   * all items on all pages.
   * @returns {Object} with named properties: start, end, total,index (if index, this means this is an index page, not a search, so the 'viewing...' should adjust
   */
  Filter_prototype.getRange = function() {
    //var objRange = {start:1,end:i+1,total:i+1}
    var strHTML = this._html;
    if(myCountReg.exec(strHTML)) {
      //page with results
      var s1 = (RegExp.$1) ? RegExp.$1 : '0';
      var s2 = (RegExp.$2) ? RegExp.$2 : '0';
      var s3 = (RegExp.$3) ? RegExp.$3 : '0';
      return {start:parseInt(s2),end:parseInt(s3),total:parseInt(s1),index:false};
    } else if(myIndexPageCountReg.exec(strHTML)) {
      //indexed page (the list server)
      var s1 = (RegExp.$1) ? RegExp.$1 : '0';
      return {start:1,end:100,total:0,index:true};
    } else {
      //no results
      return {start:0,end:0,total:0,index:false};
    }
  };

});



